调试 Common Lisp代码

Table of Contents

Common Lisp 标准是允许自定义调试代码的,在 Slime 中触发异常后会自动调用它自己的调试器 SLDB。

现代的 Common Lisp 实现都对函数编译时做了高质量的优化,比如 SBCL 默认情况下会对函数进行编译,编译后的代码会影响调试。因此,我们在调试代码的时候不能使用优化编译过的函数。有两种方法:

一个是全局执行,这样每个函数都不会被优化:

(proclaim '(optimize (debug 3)))

二是在函数内使用 declare:

(defun test ()
  (declare (optimize (debug 3)))
  ...)

这时我们再对函数进行编译,就可以在 REPL 中调试了。注意,如果要调试的函数在执行上面代码之前就编译了的话,需要重新编译一次。

Common Lisp 提供了 break 函数可以便于我们下断点,如:

(defun test1 ()
  (declare (optimize (debug 3)))
  (break) ;; 下断点
  (loop for i from 0 to 10
     do
       (print i)))

当代码执行到断点代码时,会触发 SLDB。

SLDB 最常用的几个快捷键:

详细的请见 Slime 中文文档:http://slime-user-manual-cn.readthedocs.org/en/latest/chapter-4.html

1. step 宏

step 宏可以单步执行表达式,虽然 Common Lisp 标准定义了 step 函数,但并不是每个 CL 都实现了该函数;像 SBCL 对函数编译做了优化,执行步骤也不是预期的。Lispworks 和 CLISP 中,step 宏很好用。

在 SBCL 中,需要在函数内用 declare 把 debug 等级调高,重新编译,再调用 step:

(defun string-find (main-str str)
  (declare (optimize (debug 3) (speed 0)))
  (when (> (length main-str) 0)
    (search str main-str)))

然后在 REPL 中单步执行:

(step (string-find "hello" "o"))

2. trace 宏

用来跟踪函数调用,定义如下:

(trace &rest specs)

比如定义一个斐波那契数列:

(defun fab (n)
  (cond ((equal 0 n) 0)
        ((equal 1 n) 1)
        (t (+ (fab (- n 1))
              (fab (- n 2))))))

然后对 fab 函数进行调用跟踪:

(trace fab)

调用 trace 函数后再执行它,就可以看到函数调用过程:

(fab 5)
;; 输出:
;;   0: (FAB 5)
;;     1: (FAB 4)
;;       2: (FAB 3)
;;         3: (FAB 2)
;;           4: (FAB 1)
;;           4: FAB returned 1
;;           4: (FAB 0)
;;           4: FAB returned 0
;;         3: FAB returned 1
;;         3: (FAB 1)
;;         3: FAB returned 1
;;       2: FAB returned 2
;;       2: (FAB 2)
;;         3: (FAB 1)
;;         3: FAB returned 1
;;         3: (FAB 0)
;;         3: FAB returned 0
;;       2: FAB returned 1
;;     1: FAB returned 3
;;     1: (FAB 3)
;;       2: (FAB 2)
;;         3: (FAB 1)
;;         3: FAB returned 1
;;         3: (FAB 0)
;;         3: FAB returned 0
;;       2: FAB returned 1
;;       2: (FAB 1)
;;       2: FAB returned 1
;;     1: FAB returned 2
;;   0: FAB returned 5
;; 5

trace 函数如果不跟参数,列出当前被 trace 过的函数:

(trace)                                 ; => (FAB)

用 untrace 函数可以解除跟踪。